package org.andromda.cartridges.jsf.component;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.component.UIComponentBase;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.validator.Validator;

import org.andromda.cartridges.jsf.utils.ComponentUtils;
import org.andromda.cartridges.jsf.validator.JSFValidator;
import org.andromda.cartridges.jsf.validator.JSFValidatorException;
import org.andromda.cartridges.jsf.validator.ValidatorMessages;
import org.andromda.utils.StringUtilsHelper;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.validator.Arg;
import org.apache.commons.validator.Field;
import org.apache.commons.validator.Form;
import org.apache.commons.validator.ValidatorAction;
import org.apache.commons.validator.ValidatorResources;

/**
 * A JSF component that enabled the commons-validator server side validation, as
 * well as encodes JavaScript for all client-side validations specified in the
 * same JSP page (with <code>jsf:validator</code>.
 */
public class JSFValidatorComponent extends UIComponentBase {
	private static final Log logger = LogFactory
			.getLog(JSFValidatorComponent.class);

	/**
	 * A map of validators, representing all of the Commons Validators attached
	 * to components in the current component hierarchy. The keys of the map are
	 * validator type names. The values are maps from IDs to JSFValidator
	 * objects.
	 */
	private final Map validators = new LinkedHashMap();

	private static final String JAVASCRIPT_UTILITIES = "javascriptUtilities";

	/**
	 * The attribute storing whether or not client-side validation shall
	 * performed.
	 */
	public static final String CLIENT = "client";

	/**
	 * Stores all forms found within this view.
	 */
	private final Collection forms = new ArrayList();

	/**
	 * Used to keep track of whether or not the validation rules are present or
	 * not.
	 */
	private static final String RULES_NOT_PRESENT = "validationRulesNotPresent";

	/**
     *
     */
	public JSFValidatorComponent() {
		// - default constructor for faces-config.xml
	}

	/**
	 * Registers a validator according to type and id.
	 * 
	 * @param type
	 *            The type of the validator
	 * @param id
	 *            The validator's identifier
	 * @param validator
	 *            The JSF validator associated with the id and type
	 */
	private void addValidator(final String type, final String id,
			final JSFValidator validator) {
		Map map = (Map) validators.get(type);
		if (map == null) {
			map = new LinkedHashMap();
			validators.put(type, map);
		}
		if (id != null) {
			map.put(id, validator);
		}
	}

	/**
	 * Indicates whether or not this component can be validated.
	 * 
	 * @param component
	 *            the component to check.
	 * @return canValidate true/false
	 */
	private boolean canValidate(final UIComponent component) {
		boolean canValidate = true;
		if (component != null) {
			canValidate = component.isRendered();
			if (canValidate) {
				final UIComponent parent = component.getParent();
				if (parent != null) {
					canValidate = canValidate(parent);
				}
			}
		}
		return canValidate;
	}

	/**
	 * Begin encoding for this component. This method finds all Commons
	 * validators attached to components in the current component hierarchy and
	 * writes out JavaScript code to invoke those validators, in turn.
	 * 
	 * @param context
	 *            The FacesContext for this request
	 * @throws IOException
	 */
	@Override
	public void encodeBegin(final FacesContext context) throws IOException {
		boolean validationResourcesPresent = getContextAttribute(RULES_NOT_PRESENT) == null;
		if (validationResourcesPresent
				&& JSFValidator.getValidatorResources() == null) {
			setContextAttribute(RULES_NOT_PRESENT, "true");
			validationResourcesPresent = false;
		}
		if (validationResourcesPresent) {
			try {
				validators.clear();
				forms.clear();
				// - add the javascript utilities each time
				addValidator(JAVASCRIPT_UTILITIES, null, null);
				final UIComponent form = findForm(getId());
				if (form != null) {
					findValidators(form, context, form);
					if (isClient()) {
						final ResponseWriter writer = context
								.getResponseWriter();
						writeScriptStart(context, form);
						writeValidationFunctions(form, writer, context);
						writeScriptEnd(writer);
					}
				}
			} catch (final JSFValidatorException exception) {
				logger.error(exception);
			}
		}
	}

	private UIComponent findForm(final String id) {
		UIComponent form = null;
		UIComponent validator = null;
		try {
			validator = findComponent(id);
		} catch (final NullPointerException exception) {
			// ignore - means we couldn't find the component
		}
		if (validator instanceof JSFValidatorComponent) {
			final UIComponent parent = validator.getParent();
			// When would parent ever NOT be an instance of UIComponent?
			if (parent instanceof UIComponent) {
				form = parent;
			}
		}
		return form;
	}

	/**
	 * <p>
	 * Recursively finds all Commons validators for the all of the components in
	 * a component hierarchy and adds them to a map.
	 * </p>
	 * If a validator's type is required, this method sets the associated
	 * component's required property to true. This is necessary because JSF does
	 * not validate empty fields unless a component's required property is true.
	 * 
	 * @param component
	 *            The component at the root of the component tree
	 * @param context
	 *            The FacesContext for this request
	 * @param form
	 *            the id of the form.
	 */
	private void findValidators(final UIComponent component,
			final FacesContext context, final UIComponent form) {
		if (component instanceof EditableValueHolder && canValidate(component)) {
			final EditableValueHolder valueHolder = (EditableValueHolder) component;
			if (form != null) {
				final String formId = form.getId();
				final String componentId = component.getId();
				final ValidatorResources resources = JSFValidator
						.getValidatorResources();
				if (resources != null) {
					final Form validatorForm = resources.getForm(
							Locale.getDefault(), formId);
					if (validatorForm != null) {
						final List validatorFields = validatorForm.getFields();
						for (final Iterator iterator = validatorFields
								.iterator(); iterator.hasNext();) {
							final Field field = (Field) iterator.next();

							// we need to make it match the name of the id on
							// the jsf components (if its nested).
							final String fieldProperty = StringUtilsHelper
									.lowerCamelCaseName(field.getProperty());
							if (componentId.equals(fieldProperty)) {
								for (final Iterator dependencyIterator = field
										.getDependencyList().iterator(); dependencyIterator
										.hasNext();) {
									final String dependency = (String) dependencyIterator
											.next();
									final ValidatorAction action = JSFValidator
											.getValidatorAction(dependency);
									if (action != null) {
										final JSFValidator validator = new JSFValidator(
												formId, action);
										final Arg[] args = field
												.getArgs(dependency);
										if (args != null) {
											for (final Iterator varIterator = field
													.getVars().keySet()
													.iterator(); varIterator
													.hasNext();) {
												final String name = (String) varIterator
														.next();
												validator
														.addParameter(
																name,
																field.getVarValue(name));
											}
											validator
													.setArgs(ValidatorMessages
															.getArgs(
																	dependency,
																	field));
											addValidator(
													dependency,
													component
															.getClientId(context),
													validator);
											if (!validatorPresent(valueHolder,
													validator)) {
												valueHolder
														.addValidator(validator);
											}
										}
									} else {
										logger.error("No validator action with name '"
												+ dependency
												+ "' registered in rules files '"
												+ JSFValidator.RULES_LOCATION
												+ '\'');
									}
								}
							}
						}
					}
				}
			}
		}
		for (final Iterator iterator = component.getFacetsAndChildren(); iterator
				.hasNext();) {
			final UIComponent childComponent = (UIComponent) iterator.next();
			findValidators(childComponent, context, form);
		}
	}

	private Object getContextAttribute(final String attributeName) {
		return ComponentUtils.getAttribute(FacesContext.getCurrentInstance()
				.getExternalContext().getContext(), attributeName);
	}

	/**
	 * Returns the component's family. In this case, the component is not
	 * associated with a family, so this method returns null.
	 * 
	 * @return null
	 */
	@Override
	public String getFamily() {
		return null;
	}

	/**
	 * Returns the name of the JavaScript function, specified in the JSP page
	 * that validates this JSP page's form.
	 * 
	 * @param action
	 *            the validation action from which to retrieve the function
	 *            name.
	 */
	private String getJavaScriptFunctionName(final ValidatorAction action) {
		String functionName = null;
		final String javascript = action.getJavascript();
		if (StringUtils.isNotBlank(javascript)) {
			final String function = "function ";
			final int functionIndex = javascript.indexOf(function);
			functionName = javascript.substring(functionIndex + 9);
			functionName = functionName.substring(0, functionName.indexOf('('))
					.replaceAll("[\\s]+", " ");
		}
		return functionName;
	}

	/**
	 * The component renders itself; therefore, this method returns null.
	 * 
	 * @return null
	 */
	@Override
	public String getRendererType() {
		return null;
	}

	/**
	 * Gets whether or not client side validation shall be performed.
	 * 
	 * @return true/false
	 */
	private boolean isClient() {
		final String client = (String) getAttributes().get(CLIENT);
		return StringUtils.isBlank(client) ? true : Boolean.valueOf(client)
				.booleanValue();
	}

	/**
	 * Sets whether or not client-side validation shall be performed.
	 * 
	 * @param functionName
	 *            String
	 */
	public void setClient(final String functionName) {
		getAttributes().put(CLIENT, functionName);
	}

	private void setContextAttribute(final String attributeName,
			final String attributeValue) {
		ComponentUtils.setAttribute(FacesContext.getCurrentInstance()
				.getExternalContext().getContext(), attributeName,
				attributeValue);
	}

	/**
	 * Indicates whether or not the JSFValidator instance is present and if so
	 * returns true.
	 * 
	 * @param valueHolder
	 *            the value holder on which to check if its present.
	 * @param validator
	 * @return true/false
	 */
	private boolean validatorPresent(final EditableValueHolder valueHolder,
			final Validator validator) {
		boolean present = false;
		if (validator != null) {
			final Validator[] validators = valueHolder.getValidators();
			if (validators != null) {
				for (final Validator test : validators) {
					if (test instanceof JSFValidator) {
						present = test.toString().equals(validator.toString());
						if (present) {
							break;
						}
					}
				}
			}
		}
		return present;
	}

	/**
	 * Writes the JavaScript parameters for the client-side validation code.
	 * 
	 * @param writer
	 *            A response writer
	 * @param context
	 *            The FacesContext for this request
	 * @param id
	 *            String
	 * @param validator
	 *            The Commons validator
	 */
	private void writeJavaScriptParams(final ResponseWriter writer,
			final FacesContext context, final String id,
			final JSFValidator validator) throws IOException {
		writer.write("new Array(\"");
		writer.write(id);
		writer.write("\", \"");
		writer.write(validator.getErrorMessage(context));
		writer.write("\", new Function(\"x\", \"return {");
		final Map parameters = validator.getParameters();
		for (final Iterator iterator = parameters.keySet().iterator(); iterator
				.hasNext();) {
			final String name = (String) iterator.next();
			writer.write(name);
			writer.write(":");
			final boolean mask = "mask".equals(name);

			// - mask validator does not construct regular expression
			if (mask) {
				writer.write("/");
			} else {
				writer.write("'");
			}
			final Object parameter = parameters.get(name);
			writer.write(parameter.toString());
			if (mask) {
				writer.write("/");
			} else {
				writer.write("'");
			}
			if (iterator.hasNext()) {
				writer.write(",");
			}
		}
		writer.write("}[x];\"))");
	}

	/**
	 * Write the end of the script for client-side validation.
	 * 
	 * @param writer
	 *            A response writer
	 */
	private void writeScriptEnd(final ResponseWriter writer) throws IOException {
		writer.endElement("script");
	}

	/**
	 * Write the start of the script for client-side validation.
	 * 
	 * @param writer
	 *            A response writer
	 */
	private final void writeScriptStart(final FacesContext context,
			final UIComponent component) throws IOException {
		final ResponseWriter writer = context.getResponseWriter();
		final String id = component.getClientId(context);
		writer.startElement("script", component);
		writer.writeAttribute("type", "text/javascript", null);
		writer.writeAttribute("id", id + ":validation-code", null);
	}

	/**
	 * writes the javascript functions to the response.
	 * 
	 * @param form
	 *            UIComponent
	 * @param writer
	 *            A response writer
	 * @param context
	 *            The FacesContext for this request
	 * @throws IOException
	 */
	private final void writeValidationFunctions(final UIComponent form,
			final ResponseWriter writer, final FacesContext context)
			throws IOException {
		writer.write("var bCancel = false;\n");
		writer.write("self.validate" + StringUtils.capitalize(form.getId())
				+ " = ");
		writer.write("function(form) { return bCancel || true\n");

		// - for each validator type, write "&& fun(form);
		final Collection validatorTypes = new ArrayList(validators.keySet());

		// - remove any validators that don't have javascript functions defined.
		for (final Iterator iterator = validatorTypes.iterator(); iterator
				.hasNext();) {
			final String type = (String) iterator.next();
			final ValidatorAction action = JSFValidator
					.getValidatorAction(type);
			final String functionName = getJavaScriptFunctionName(action);
			if (StringUtils.isBlank(functionName)) {
				iterator.remove();
			}
		}

		for (final Iterator iterator = validatorTypes.iterator(); iterator
				.hasNext();) {
			final String type = (String) iterator.next();
			final ValidatorAction action = JSFValidator
					.getValidatorAction(type);
			if (!JAVASCRIPT_UTILITIES.equals(type)) {
				writer.write("&& ");
				writer.write(getJavaScriptFunctionName(action));
				writer.write("(form)\n");
			}
		}
		writer.write(";}\n");

		// - for each validator type, write callback
		for (final Iterator iterator = validatorTypes.iterator(); iterator
				.hasNext();) {
			final String type = (String) iterator.next();
			final ValidatorAction action = JSFValidator
					.getValidatorAction(type);
			String callback = action.getJsFunctionName();
			if (StringUtils.isBlank(callback)) {
				callback = type;
			}
			writer.write("function ");
			writer.write(form.getId() + '_' + callback);
			writer.write("() { \n");

			// for each field validated by this type, add configuration object
			final Map map = (Map) validators.get(type);
			int ctr = 0;
			for (final Iterator idIterator = map.keySet().iterator(); idIterator
					.hasNext(); ctr++) {
				final String id = (String) idIterator.next();
				final JSFValidator validator = (JSFValidator) map.get(id);
				writer.write("this[" + ctr + "] = ");
				writeJavaScriptParams(writer, context, id, validator);
				writer.write(";\n");
			}
			writer.write("}\n");
		}

		// - for each validator type, write code
		for (final Iterator iterator = validatorTypes.iterator(); iterator
				.hasNext();) {
			final String type = (String) iterator.next();
			final ValidatorAction action = JSFValidator
					.getValidatorAction(type);
			writer.write(action.getJavascript());
			writer.write("\n");
		}
	}
}
